
DEBUG = False

import os, re, sys
from amplitude.dict import Dict
from lxml import etree, cssselect
import cssutils
#assert '0.9.7' in cssutils.__version__


class CSS(Dict):

    def __init__(self, fn='', validate=False, loglevel=50, **parser_args):
        # load the css from fn
        if os.path.exists(fn):
            self.fn = fn.replace("\\", "/")
            self.doc = cssutils.CSSParser(validate=validate, loglevel=loglevel, **parser_args).parseFile(fn)
        else:
            self.doc = cssutils.CSSParser(validate=validate, loglevel=loglevel, **parser_args).parseString(fn)
            
    def load(self, css, **parser_args):
        if os.path.isfile(css): # it's a css filename
            self.doc = cssutils.CSSParser(**parser_args).parseFile(css)
        else:                   # it's the text of the css
            self.doc = cssutils.CSSParser(**parser_args).parseString(css)
    
    def write(self, fn=None):
        fn = fn or self.fn
        t = self.doc.cssText
        t = re.sub("\}\n", "}\n\n", t)      # I like blank spaces between css rules.
        f = open(fn, 'wb')
        f.write(t.encode('utf-8'))
        f.close()

    def replace_or_add(self, rule):
        """replace the rule if it exists, or add it if it doesn't"""
        print >> sys.stdout, "CSS.replace_or_add(rule) not yet implemented. Do the following rule by hand:"
        print rule.cssText        

    def add_selectors_from(self, cssfns, verbose=False):
        """add any selectors from cssfn that are missing in this CSS file -- append to the end"""
        if type(cssfns) in [str, unicode]:
            cssfns = [cssfns]
        elif type(cssfns) != list:
            raise TypeError, 'invalid type for cssfns parameter:' + str(type(cssfns))

        self_rules = [r for r in self.doc.cssRules if r.typeString=='STYLE_RULE']
        self_selectors = []
        for rule in self_rules:
            for sel in rule.selectorList:
                if sel.selectorText not in self_selectors: self_selectors.append(sel.selectorText)

        for cssfn in cssfns:
            if verbose==True: print cssfn
            css = CSS(cssfn)
            css_rules = [r for r in css.doc.cssRules if r.typeString=='STYLE_RULE']
            css_selectors = []
            for rule in css_rules:
                for sel in rule.selectorList:
                    if sel.selectorText not in css_selectors: css_selectors.append(sel.selectorText)
            for sel in [sel for sel in css_selectors if sel not in self_selectors]:
                new_rule = cssutils.css.CSSStyleRule(selectorText=sel, style=rule.style)
                self.doc.cssRules.append(new_rule)
                self_rules.append(new_rule)
                self_selectors.append(sel)
                if verbose==True: print ' ', sel

    def compare_style_rules_to(self, css2fn, ignore_not_found=False, ignore_style_override=False, ignore_multiple_rules=False):
        """Compare two stylesheets to see what differences there are in what style rules are defined, and how.
        Ignore all other rule types: comments, @import, @media, and @font-face rules."""
        css2 = CSS(css2fn)
        for rule in [rule for rule in self.doc.cssRules if rule.typeString=='STYLE_RULE']:
            for sel in rule.selectorList:
                if ignore_style_override==True and 'style-override' in sel.selectorText: continue
                if DEBUG==True: print sel.selectorText
                rules2 = css2.find_style_rules(sel.selectorText, exact=True)
                if len(rules2)==0:
                    if ignore_not_found==False:
                        print '', sel.selectorText, '-- NOT FOUND'
                elif len(rules2)>1:
                    if  ignore_multiple_rules==False:
                        print '', sel.selectorText, '--', len(rules2), "RULES FOUND"
                else:
                    style2 = re.sub("\s+", ' ', rules2[0].style.cssText).strip()
                    style1 = re.sub("\s+", ' ', rule.style.cssText).strip()
                    if style1 != style2:
                        print '', sel.selectorText, '-- STYLE DIFFERENCE:'
                        print "   1:", style1
                        print "   2:", style2

    def find_style_rules(self, selector, exact=False):
        """Find the rules that match the given selector.
        If exact==True, then the given selector has to match one of the rule's selectors exactly;
        if exact==False, then the given selector can be _part_ of one of the rule's selectors."""
        
        if exact==True:
            rules = [rule for rule in self.doc.cssRules 
                    if rule.typeString=='STYLE_RULE'
                    and selector in [sel.selectorText for sel in rule.selectorList]]
        else:
            rules = [rule for rule in self.doc.cssRules
                    if rule.typeString=='STYLE_RULE'
                    and [sel for sel in rule.selectorList if selector in sel.selectorText] != []]
        return rules

    def embed_selector_style_in(self, xmlfn, selector, exact=False, xmlns=None):
        "embed the style of the given selector in the xml elements to which it pertains"
        from .xml import XML
        xmldoc = XML(xmlfn)
        rules = self.find_style_rules(selector, exact=exact)
        for rule in rules:
            style = re.sub("\s+", ' ', rule.style.cssText).strip().replace(': ',':').replace("'",'').replace('"','')
            for sel in rule.selectorList:
                xpath = "//"+self.selector_to_xpath(sel.selectorText, xmlns=xmlns)
                for elem in xmldoc.root.xpath(xpath):
                    elem.set('style', style)
        xmldoc.write()

    # == CLASS METHODS ==
        
    @classmethod
    def selector_to_xpath(cls, selector, nsprefix=None, custom_parser=False):
        """convert a css selector into an xpath expression. 
        xmlns is optional href"""

        if type(selector) == cssutils.css.Selector:
            selectorText = selector.selectorText
        else:
            selectorText = selector

        if custom_parser:
            parser = XPathParser()
            selectorText = parser.preprocess(selectorText, nsprefix)
            
        path = cssselect.CSSSelector(selectorText).path

        if custom_parser:
            path = parser.postprocess(path, nsprefix)
        else:
            path = re.sub("/?descendant(-or-self)?::",'//', path)
            if nsprefix is not None:
                path = re.sub("(//?)", r"\1%s:" % nsprefix, path)
        if DEBUG==True: 
            print '', selectorText        
            print ' ==>', path
        # custom = XPathParser()
        # path = custom.postprocess(path, 'xhtml')

        return path

    @classmethod
    def embed_css_selector_styles_in(cls, cssfn, xmlfn, selector, exact=False, xmlns=None):
        c = cls(cssfn)
        for rule in c.find_style_rules(selector, exact=exact):
            for sel in rule.selectorList:
                c.embed_selector_style_in(xmlfn, sel.selectorText, exact=exact, xmlns=xmlns)

CSSFile = CSS

class XPathParser():
    """Custom parsing for cleaning up xpath returned by cssselect"""
    def __init__(self):
        pass

    def preprocess(self, selectorText, nsprefix):
        """
        Alters the selectorText so that it produces the correct result
        """
        # fix up selectors without specified elements
        selectorText = selectorText.replace(' .', ' *.')
        if selectorText[0] == '.':
            selectorText = '*' + selectorText
        if '#' in selectorText[0]:
            selectorText = selectorText.replace('#', '*#')
        # insert the nsprefix where needed
        if nsprefix:
            # if '>' in selectorText:
            #     print selectorText
            delimiters = ('>', '+')
            selectorText = ' '.join([
                (n.strip() not in delimiters and nsprefix + '|' + n.strip() or n.strip())
                for n in selectorText.split(' ')
                ])
            selectorText = selectorText.replace('+', '+%s|' % nsprefix)# add the prefix in after adjacent sibling selector
            # if '>' in selectorText:
            #     print selectorText
        return selectorText

    def postprocess(self, xpath, nsprefix):
        """
        Cleans up the xpath returned by cssselect
        """
        self.xpath = xpath
        self.nsprefix = nsprefix
        self.xpath_dict = self.parse(xpath)
        # Make the xpath shorter
        # PleasePrint = False
        # if '/descendant-or-self::*/' in self.xpath:
        #     print self.xpath
        #     PleasePrint = True
        self.fix_descendant_or_self()
        # if PleasePrint:
        #     print self.translate()
        self.fix_descendant()
        # if PleasePrint:
        #     print self.translate()
        # cssselect produces broken xpath when it processes an adjacent
        # sibling selector. Following method fixes it.
        self.fix_following_sibling()
        return self.translate()

    def translate(self):
        """
        assemble the outputs in the xpath_dict after postprocess
        """
        result = []
        for i in range(len(self.xpath_dict)):
            result.append(self.xpath_dict[i]['output'])
        return '/'.join(result)

    def fix_descendant_or_self(self):
        """
        change /descendant-or-self::*/ to //
        """
        # if self.nsprefix:
        #     node_to_zap = '%s:*' % self.nsprefix
        # else:
        #     node_to_zap = '*'
        for key in self.xpath_dict.iterkeys():
            val = self.xpath_dict[key]
            if DEBUG == True: print val
            if val['axis'] == 'descendant-or-self':
                if val['element'] == '*':#node_to_zap:
                    self.xpath_dict[key]['output'] = ''
                else:
                    self.xpath_dict[key]['output'] = '%s%s' % (val['element'], ''.join(val['predicates']))
            if DEBUG == True: print '  => %s' % self.xpath_dict[key]['output']

    def fix_descendant(self):
        """
        change descendant-or-self::something_specific to something_specific
        """
        for key in self.xpath_dict.iterkeys():
            val = self.xpath_dict[key]
            if val['axis'] == 'descendant':
                self.xpath_dict[key]['output'] = '%s%s' % (val['element'], ''.join(val['predicates']))

    def fix_following_sibling(self):
        """
        change something/following-sibling::* etc to something[preceding-sibling::]
        rearrange as follows:
        the node with following-sibling becomes target and comes first
        the node before following-sibling becomes a predicate with preceding-sibling
        any others become predicates with the appropriate relationship (descendant flips to ancestor)
        """
        if 'following-sibling' in self.xpath:
            target_key = [key for key
                in self.xpath_dict.iterkeys()
                if self.xpath_dict[key]['axis'] == 'following-sibling'][0]
            target = self.xpath_dict[target_key]
            preceding = self.xpath_dict[target_key - 1]
            target_element = self.predicates_to_element(target['predicates'])[0]
            target_class = self.predicates_to_element(target['predicates'])[1]
            preceding_class = [p for p in preceding['predicates'] if 'contains' in p][0]
            preceding_predicate = '[preceding-sibling::%s[1]%s]' % (preceding['element'], preceding_class)
            output = '%s%s%s' % (target_element, target_class, preceding_predicate)
            if target_key == 3:
            # I don't know how to more reliably figure out if there's an ancestor.
                ancestor = self.xpath_dict[target_key - 3]
                # print ancestor
                ancestor_class = [p for p in ancestor['predicates'] if 'contains' in p][0]
                ancestor_predicate = '[ancestor::%s%s]' % (ancestor['element'], ancestor_class)
                output += ancestor_predicate
            self.xpath_dict = {0: {'output': output}}
            # print output

    def predicates_to_element(self, predicates):
        """
        Given the predicates that cssselect generates to identify a following-sibling,
        return the element and class
        """
        # print predicates
        element_pred = [p for p in predicates if 'name()' in p][0]
        element = element_pred[element_pred.find("'") + 1:element_pred.rfind("'")]
        class_pred = [p for p in predicates if 'contains' in p][0]
        return (element, class_pred)

    def parse(self, xpath):
        """
        Returns a dictionary.

        """
        result = {}
        nodes = xpath.split('/')
        for i in range(len(nodes)):
            result[i] = self.parse_node(nodes[i])
        return result

    def parse_node(self, node):
        result = {}
        result['axis'] = self._axis(node)
        result['predicates'] = self._predicates(node)
        result['element'] = self._element(node)
        result['output'] = node
        return result
        
    def _predicates(self, node):
        parts = node.split(' and ')
        new_node = ']['.join(parts)
        pat = r'\[[^\]]+\]'
        return re.findall(pat, new_node)

    def _element(self, node):
        if '::' in node:
            node = node[node.find('::')+2:]
        pat = r'\[[^\]]+\]'# delete predicates
        node = re.sub(pat, '', node)
        return node

    def _axis(self, node):
        halves = node.split('::')
        if len(halves) > 1:
            return halves[0]
        return None